﻿using Nop.Core.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Logistics.Services
{
    public class LogisticsService
    {
#if false        
        #region Fields

        private readonly IRepository<Address> _addressRepository;
        private readonly IRepository<Contactor> _contactorRepository;
        private readonly IRepository<Submitter> _submitterRepository;

        private ShipmentManager _shipmentManager;

        #endregion

        public string ImportShipment(
            Nop.Core.Domain.Common.Address deliveryAddress,
            Nop.Core.Domain.Customers.Customer shipmentOwner,
            ShipTimeWindow ownerPreferredTime,
            ShipTimeWindow receiverPreferredTime,
            Nop.Core.Domain.Common.Address pickupAddress = null)
        {
            // TODO: What is the appropriate store address in the future?
            // Currently user a shipmentOwner's billing address as the store address.
            // (I presume now that a store owner will have a special user account to operate a store).
            var ownerAddress = shipmentOwner.Addresses.First();
            var dAddress = pickupAddress == null ? ownerAddress : pickupAddress;

            if (deliveryAddress == null || dAddress == null || ownerAddress == null)
            {
                throw new ArgumentException("The shipment information is not valid.", "shipment|shipmentOwner");
            }

            var sp = ShipmentManager.WrapShipment(this,
                ShipmentManager.GenerateAddress(this, dAddress),
                ShipmentManager.GenerateAddress(this, deliveryAddress),
                new Contactor() // Pick up
                {
                    UserInformation = new UserInformation()
                    {
                        ExternalId = shipmentOwner.Id,
                        FirstName = ownerAddress.FirstName,
                        LastName = ownerAddress.LastName,
                        Account = shipmentOwner.Email,
                    },
                    PhoneNumber = ownerAddress.PhoneNumber,
                    PreferredTimeWindow = TimeWindow.SevenAmToTenAm
                },
                new Contactor() // Delivery
                {
                    UserInformation = new UserInformation()
                    {
                        ExternalId = deliveryAddress.Id,
                        FirstName = deliveryAddress.FirstName,
                        LastName = deliveryAddress.LastName,
                        Account = deliveryAddress.Email,
                    },
                    PhoneNumber = deliveryAddress.PhoneNumber,
                    PreferredTimeWindow = TimeWindow.TenAmToThreePm
                },
                new Submitter()
                {
                    UserInformation = new UserInformation()
                    {
                        ExternalId = shipmentOwner.Id,
                        FirstName = dAddress.FirstName,
                        LastName = dAddress.LastName,
                        Account = shipmentOwner.Email,
                    }
                });

            return sp.Id.ToString() + "-" + sp.TrackingNumber;
        }

        public bool GetIdByExternalId(int externalId, out int id)
        {
            // TODO: Index this id and add foreign key
            var users = new List<UserBase>();
            users.AddRange(Drivers);
            users.AddRange(Contactors);
            users.AddRange(Set<Submitter>());
            foreach (var u in users)
            {
                if (u.UserInformation.ExternalId.HasValue && u.UserInformation.ExternalId == externalId)
                {
                    id = u.Id;
                    return true;
                }
            }
            id = 0;
            return false;
        }

        public ICollection<Locality> GetCourierPreferredLocalitiesByExternalId(int externalId)
        {
            int id;
            if (GetIdByExternalId(externalId, out id))
            {
                Driver d = Drivers.Find(id);
                if (d != null)
                {
                    return d.PreferredLocalities;
                }
            }
            return null;
        }

        public void DemoteDriver(Customer courier)
        {
            int id;
            if (GetIdByExternalId(courier.Id, out id))
            {
                Driver d = Drivers.Find(id);
                if (d != null)
                {
                    Set<UserInformation>().Remove(d.UserInformation);
                    Drivers.Remove(d);
                    SaveChanges();
                }
            }
        }

        public Shipment GetShipmentById(int shipmentId)
        {
            return Shipments.Where(s => s.Id == shipmentId).Single();
        }

        public RoutePlan GetPlanByDriverId(int driverId)
        {
            return RoutePlans.Where(r => r.Status != RouteStatus.Completed && r.DriverId == driverId).FirstOrDefault();
        }

        public IEnumerable<Driver> GetAvailableDrivers()
        {
            var availableDrivers = new List<Driver>();
            foreach (var d in Drivers)
            {
                try
                {
                    var rp = RoutePlans.SingleOrDefault(
                        r => r.DriverId == d.Id && r.Status != RouteStatus.Completed);
                    if (null == rp)
                    {
                        availableDrivers.Add(d);
                    }
                }
                catch (InvalidOperationException e)
                {
                    throw new InvalidOperationException(
                        String.Format("A driver has been assigned more than one route. Driver: {0}:{1}", d.Id, d.DisplayName), e);
                }
            }
            return availableDrivers;
            // The following code throws NotSupportException
            //return Drivers.Where(d => (
            //    RoutePlans.SingleOrDefault(
            //    r => r.Status != RouteStatus.Completed && r.DriverId == d.Id) == null
            //    )).ToArray();
        }

        public IEnumerable<Driver> GetCurrentPlannedDrivers()
        {
            return RoutePlans.Where(rp => rp.Status == RouteStatus.NewlyPlanned).Select(rp => rp.Driver);
        }

        public IEnumerable<Package> GetPendingPlanPackages()
        {
            var w = Warehouses.First();
            var pkgs = Packages.Where(
                p => (
                    (p.Status == PackageStatus.ReadyForPickUp || p.Status == PackageStatus.InWarehouse)
                    && (p.Shipment.Status == ShipmentStatus.Pending)
                ));
            pkgs.ToList().ForEach(p => p.Warehouse = w);
            return pkgs;
        }

        public void SetPackageAsPlanned(IEnumerable<Package> plannedPackages)
        {
            var pkgs = new List<Package>(plannedPackages);

            var plans = from p in RoutePlans
                        where p.Status != RouteStatus.Completed
                        select p;

            foreach (var p in plans)
            {
                foreach (var l in p.AssignedLocations)
                {

                    if (pkgs.Find(pk => pk.Id == l.Shipment.Package.Id) != null)
                    {
                        p.Driver.Packages.Add(l.Shipment.Package);
                        l.Shipment.Status = ShipmentStatus.InProgress;
                        pkgs.Remove(l.Shipment.Package);
                    }
                    l.AssignedDriver = p.Driver;
                    TimeUtility.UpdateTimeStamp(l);
                }
            }

            SaveChanges();

            if (pkgs.Count > 0)
            {
                throw new InvalidOperationException("Some packages are not planned yet.");
            }
        }

        public void SavePlan(PlanType plan)
        {
            // TODO: Verify plan, resolve warehouse and so on
            Warehouse w = Warehouses.Single();

            foreach (var da in plan.DriverAssignments)
            {
                if (da.GoWaypoints == null && da.ReturningWaypoints == null && da.UnresolvedWaypoints == null)
                {
                    continue;
                }

                var wps = new List<Location>();
                if (da.GoWaypoints != null)
                {
                    wps.AddRange(da.GoWaypoints.OrderBy(wp => wp.Rank));
                }
                if (da.ReturningWaypoints != null)
                {
                    //wps.AddRange(da.ReturningWaypoints.Select(a => a.Address.FormattedAddress));
                    wps.AddRange(da.ReturningWaypoints.OrderBy(wp => wp.Rank));
                }

                //TODO: Log error here if unresolved.
                if (da.UnresolvedWaypoints != null)
                {
                    //wps.AddRange(da.UnresolvedWaypoints.Select(a => a.Address.FormattedAddress));
                    wps.AddRange(da.UnresolvedWaypoints.OrderBy(wp => wp.Rank));
                }

                var r = new RoutePlan() { DriverId = da.Driver.Id };
                wps.ForEach(wp => r.AssignedLocations.Add(wp));
                r.Warehouse = w;
                RoutePlans.Add(r);
            }
            SaveChanges();
        }

        public void PromptDriver(Customer driver,
                                 String FirstName,
                                 String LastName,
                                 List<String> cityList)
        {
            if (driver == null)
                throw new ArgumentNullException("driver");

            if (String.IsNullOrEmpty(FirstName))
                throw new ArgumentNullException("FirstName");

            int id;
            Driver dr;
            if (GetIdByExternalId(driver.Id, out id))
            {
                dr = Drivers.Find(id);
                dr.UserInformation.FirstName = FirstName;
                dr.UserInformation.LastName = LastName = LastName ?? "";
                dr.UserInformation.ExternalId = driver.Id;
            }
            else
            {
                dr = new Driver()
                {
                    UserInformation = new UserInformation()
                    {
                        FirstName = FirstName,
                        LastName = LastName ?? "",
                        Account = driver.Email,
                        ExternalId = driver.Id
                    }
                };
                Drivers.Add(dr);
            }

            foreach (var city in cityList)
            {
                dr.PreferredLocalities.Clear();
                dr.PreferredLocalities.Add(Localities.Single(l => l.Name == city));
            }

            SaveChanges();
        }


        public string GetNewTrackingNumber()
        {
            return Guid.NewGuid().ToString();
        }
#endif
    }
}
